21. 多维数据可视化-绘制股票相关性热力图

本章概要

  • 学习材料:三只中国股票复权收盘价及共同交易日。
  • 本章任务:计算收益率相关矩阵并绘制热力图,固定资产顺序与同一时间段样本。
  • 完成后你将得到:收益矩阵 shape、相关矩阵、热力图和最强资产对。
  • 自我检查:用 Series.corr 另算一组股票;同时检查股票是否齐全、共同交易日是否足够,以及是否存在取值不变的列。
  • 拓展练习:把相关热力图拓展应用到另一组三只中国股票。

本章学习目标

  • 理解皮尔逊相关系数的数学定义与金融含义
  • 掌握使用 tushare 获取A股数据的方法
  • 学会计算简单收益率对数收益率
  • 掌握 seaborn.heatmap() 绘制相关性热力图
  • 了解层次聚类、网络图等高级可视化方法

相关性在金融中的意义

在现代投资组合理论中,资产间的相关性是核心概念:

  • 分散化投资:相关性越低,组合风险越小
  • 系统性风险识别:高相关性意味着暴露于共同因子
  • 配对交易:高相关性股票可能存在价差套利机会
  • 因子模型:通过相关性聚类识别行业与风格因子

皮尔逊相关系数的定义

皮尔逊相关系数衡量两个变量线性相关的强度和方向:

\[ \large \rho_{X,Y} = \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} = \frac{E[(X-\mu_X)(Y-\mu_Y)]}{\sigma_X \sigma_Y} \]

  • \(\text{Cov}(X,Y)\):协方差,衡量两变量共同变动程度
  • \(\sigma_X, \sigma_Y\):各自的标准差
  • 取值范围\([-1, 1]\)

相关系数的直观理解

相关系数范围 含义 投资启示
\([0.7, 1]\) 强正相关 同涨同跌,分散化效果差
\([0.4, 0.7)\) 中度正相关 有一定联动性
\([-0.1, 0.4)\) 弱正相关/接近零 可进一步观察其分散风险作用
\([-0.7, -0.1)\) 中度负相关 可进一步研究其对冲作用
\([-1, -0.7)\) 强负相关 检查样本、压力期与结构变化

这些区间是描述性课堂语言,不构成交易或对冲结论。样本量、置信区间、滚动稳定性、交易成本与压力期相关缺一不可;同一相关系数在 20 日与 500 日样本中的不确定性不同。

投资组合方差与相关性

投资组合方差的完整公式:

\[ \large \sigma_p^2 = \sum_i \sum_j w_i w_j \sigma_i \sigma_j \rho_{ij} \]

  • \(w_i, w_j\):各资产的权重
  • \(\rho_{ij}\):资产 \(i\)\(j\) 的相关系数
  • 核心启示:相关性越低,组合风险越小——这是马科维茨理论的核心

相关系数的局限性

相关系数只能衡量线性关系,存在重要局限:

  • 非线性关系:若 \(Y = X^2\)\(X\) 对称分布,则 \(\rho = 0\),但两者有强关系
  • 异常值敏感:极端观测值会严重扭曲相关系数
  • 不能推因果:相关不等于因果关系
  • 时变性:相关系数会随市场状态变化(牛市/熊市)

运行前预测|平台任务:获取数据并绘制热力图

  • 输入预测:运行前先写出 prostock_listdatadf 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到df 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务:获取数据并绘制热力图”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务:获取数据并绘制热力图

任务复盘|平台任务:获取数据并绘制热力图

使用提醒:平台固定答案中含有类似访问密钥的文字,只能在指定教学平台内按要求输入,不要复制到公开网页或其他程序中。本章后面的分析使用课程本地数据,不需要这类访问密钥。

依据诊断:本任务只用于平台固定文本录入。其缩进非法、没有按交易日期键连接、直接对价格水平求相关,不能支持任何金融相关性结论;有效解释只能绑定后文“共同交易日对齐→同频收益率→相关矩阵”的本地分析。

拓展应用检查|从平台录入到便于核对依据

拓展练习:把数据替换为一个中国企业或市场序列,改变一个分组或时间窗口;说明图形结论是否改变,以及为何。

  • 保留共同交易日、同频收益率与资产顺序三项口径。
  • 同时报告相关系数、样本期、样本量与窗口敏感性。
  • 若凭据未完成外部吊销/轮换与授权净化,继续执行 PUBLICATION_STOP

问题分析:为什么平台代码不能作为依据

平台代码执行流程 展示从导入库到输出热力图的五个步骤流程图 导入库 设置Token 循环获取 5只股票数据 转换为 DataFrame 错误:价格水平 且日期未对齐 热力图 Step 1 Step 2 Step 3 Step 4 Step 5 tushare/pandas seaborn/matplotlib pro.daily() 循环5只股票 pd.DataFrame .from_dict() df.corr() 皮尔逊系数 sns.heatmap()

关键函数解析:pro.daily()df.corr()

pro.daily():获取日线行情

  • ts_code:股票代码(如 '600030.SH'
  • start_date / end_date:起止日期
  • 返回包含 open/high/low/close/vol 等字段的 DataFrame

df.corr():计算相关系数矩阵

  • 默认使用皮尔逊相关系数
  • 输入 DataFrame 的每一列与其他列两两计算
  • 返回对称矩阵,对角线为 1

关键函数解析:sns.heatmap()

sns.heatmap(df.corr(), annot=True, cmap='coolwarm') 参数详解:

参数 含义 本例取值
data 输入的二维数据(相关系数矩阵) df.corr()
annot 是否在格子中标注数值 True
cmap 颜色映射方案 'coolwarm'(蓝红渐变)
center 颜色中心值 可设为 0
square 格子是否为正方形 可设为 True

常用颜色映射方案对比

名称 效果 适用场景
coolwarm 蓝→白→红 通用,正负对比清晰
RdYlGn 红→黄→绿 金融领域常用
RdBu 红→白→蓝 色盲友好
viridis 紫→绿→黄 感知均匀,适合打印
YlOrRd 黄→橙→红 只有正值时

建议:金融热力图优先使用 'RdYlGn''coolwarm',便于直观区分正负相关。

收益率的两种计算方法

在计算相关性前,通常先将价格转换为收益率

简单收益率

\[ \large R_t = \frac{P_t - P_{t-1}}{P_{t-1}} \]

对数收益率

\[ \large r_t = \ln\left(\frac{P_t}{P_{t-1}}\right) = \ln(P_t) - \ln(P_{t-1}) \]

简单收益率 vs 对数收益率

Python 实现df.pct_change()np.log(df / df.shift(1))

特性 简单收益率 对数收益率
公式 \((P_t - P_{t-1}) / P_{t-1}\) \(\ln(P_t / P_{t-1})\)
可加性 多期不可直接相加 可加:\(r_{1 \to 3} = r_1 + r_2 + r_3\)
对称性 同幅百分比涨跌不互相抵消 倒数价格因子的对数收益符号相反
分布 由样本决定 有时更接近对称,但不保证正态
应用 报告收益率 计算波动率、建模

相关性分析选择:同一频率、共同时间键下,简单收益率或对数收益率都可计算 Pearson 相关;应预先声明口径并做敏感性比较。时间可加性本身不是选择单期相关口径的理由。

收益率计算代码演示

展开完整代码(投影默认折叠)

# 计算简单收益率
# pct_change(): (当前值 - 上期值) / 上期值
simple_returns = df_prices.pct_change().dropna()

# 计算对数收益率
# ln(P_t / P_{t-1}) = ln(P_t) - ln(P_{t-1})
log_returns = np.log(df_prices / df_prices.shift(1)).dropna()

# 输出收益率预览
print('简单收益率(前5行):')
print(simple_returns.head())
print(f'\n对数收益率(前5行):')
print(log_returns.head())

# 收益率统计对比
print(f'\n简单收益率统计:')
print(simple_returns.describe().round(4))
print(f'\n对数收益率统计:')
print(log_returns.describe().round(4))

相关系数矩阵的计算


# 使用对数收益率计算相关性矩阵
# corr() 默认使用皮尔逊相关系数 (method='pearson')
corr_matrix = log_returns.corr()

# 输出相关性矩阵
print('相关性矩阵:')
print(corr_matrix.round(3))
print(f'\n矩阵形状: {corr_matrix.shape}')

# 验证对称性
print(f'\n矩阵对称性检验:')
print(f'是否对称: {np.allclose(corr_matrix, corr_matrix.T)}')
print(f'对角线元素(应全为1): {np.diag(corr_matrix)}')

相关系数矩阵的三个重要性质

  1. 对称性\(\rho_{ij} = \rho_{ji}\),矩阵等于其转置
  2. 对角线为 1\(\rho_{ii} = 1\),变量与自身完全正相关
  3. 半正定性:协方差矩阵和有效相关矩阵均为半正定,不要求严格正定;因此任意组合的二次型方差非负

corr() 的三种计算方式

参数 方法 特点
'pearson' 皮尔逊(默认) 衡量线性关系
'spearman' 斯皮尔曼秩相关 对异常值稳健
'kendall' 肯德尔 \(\tau\) 相关 适合小样本

古典 Pearson 检验:适用条件

检验原假设 \(H_0: \rho = 0\)(两变量无线性相关)

\[ \large t = \frac{r\sqrt{n-2}}{\sqrt{1-r^2}} \sim t_{n-2} \]

  • \(r\):样本相关系数;\(n\):配对观测数。
  • 精确的 \(t\) 参考分布要求独立、同分布的二元正态配对观测;还须排除常量列与严重异常值。
  • 金融收益常有序列相关、波动聚集和厚尾;条件不满足时,古典 p 值只作诊断,不作显著性依据。

金融序列推断:先选稳健路线

  1. 先画收益率、滚动相关与残差诊断,声明频率、窗口和缺失处理。
  2. 若存在序列依赖或异方差,使用保留时间结构的移动区组/平稳 bootstrap,报告置信区间;不得随机逐行重采样。
  3. 若坚持古典 Pearson 检验,必须逐项记录假设检查和适用边界。
  4. 对矩阵内全部资产对同时推断时,先对 p 值做 Benjamini–Hochberg FDR 调整,再附星号。

遇到问题时:没有合理依赖结构、重采样块长或多重检验方案时,只报告描述性相关,不发布“显著”结论。

显著性标记与决策边界

符号 FDR 调整后 q 值 含义
*** \(q < 0.01\) 在当前检验族内通过 1% FDR 判断标准
** \(q < 0.05\) 在当前检验族内通过 5% FDR 判断标准
* \(q < 0.10\) 探索性候选,须核对
ns \(q \geq 0.10\) 未通过当前 FDR 判断标准

决策边界:调整后的统计显著 \(\neq\) 经济显著。必须同时报告系数大小、置信区间、样本期与稳健性检查,不能仅凭星号作决策。

显著性检验代码演示

展开完整代码(投影默认折叠)

from scipy.stats import pearsonr  # 在古典假设成立时计算皮尔逊检验
from statsmodels.stats.multitest import multipletests  # 对矩阵内多重检验执行FDR调整

n = len(corr_matrix)
pairs = []
for i in range(n):
    for j in range(i+1, n):
        stock1 = corr_matrix.index[i]
        stock2 = corr_matrix.columns[j]
        corr = corr_matrix.iloc[i, j]
        # pearsonr() 返回 (相关系数, p值)
        _, p_value = pearsonr(
            log_returns[stock1], log_returns[stock2]
        )
        pairs.append({
            '股票1': stock1,
            '股票2': stock2,
            '相关系数': corr,
            '原始P值': p_value
        })

df_pairs = pd.DataFrame(pairs)  # 汇总同一相关矩阵内的全部资产对
df_pairs['FDR_q值'] = multipletests(df_pairs['原始P值'], method='fdr_bh')[1]  # 控制检验族的错误发现率
df_pairs['显著性'] = pd.cut(df_pairs['FDR_q值'], bins=[-1, 0.01, 0.05, 0.10, 1], labels=['***', '**', '*', 'ns'])  # 依据调整后q值生成探索性标记
df_pairs = df_pairs.sort_values('相关系数', ascending=False)  # 按相关强度排列输出
print(df_pairs.to_string(index=False))  # 同时展示效应量、原始p值与FDR结果

绘制基础热力图(完整版)

展开完整代码(投影默认折叠)

import seaborn as sns

plt.figure(figsize=(10, 8))
sns.heatmap(
    corr_matrix,         # 相关系数矩阵
    annot=True,          # 显示数值
    cmap='RdYlGn',       # 红-黄-绿配色
    center=0,            # 颜色中心值为0
    square=True,         # 正方形格子
    linewidths=0.5,      # 格子边框
    fmt='.3f',           # 保留3位小数
    cbar_kws={'label': '皮尔逊相关系数'},
    vmin=-1, vmax=1      # 固定范围[-1,1]
)
plt.title('股票收益率相关性热力图', fontsize=14, pad=20)
plt.xlabel('股票', fontsize=12)
plt.ylabel('股票', fontsize=12)
plt.tight_layout()
plt.show()

热力图参数详解

热力图参数效果 展示sns.heatmap主要参数及其对热力图效果的影响 sns.heatmap() 核心参数 annot=True 格内显示数值 cmap='RdYlGn' 选择颜色映射 center=0 0 为正负中点 square=True 格子保持正方 fmt='.3f' 保留 3 位小数 linewidths=0.5 格子间边框宽 vmin=-1, vmax=1 固定理论范围 cbar_kws={...} 定义颜色条标签

进阶:层次聚类热力图

sns.clustermap() 在热力图基础上添加树状图,将相似股票自动聚在一起:

展开完整代码(投影默认折叠)

from scipy.cluster.hierarchy import linkage

linkage_matrix = linkage(corr_matrix, method='average')
g = sns.clustermap(
    corr_matrix, annot=True, cmap='RdYlGn',
    center=0, square=True, linewidths=0.5,
    fmt='.3f', figsize=(10, 10),
    row_linkage=linkage_matrix,
    col_linkage=linkage_matrix,
    tree_kws={'linewidths': 1.5}
)
g.fig.suptitle('股票相关性聚类分析', fontsize=14, y=0.98)
plt.show()

层次聚类的原理与应用

距离定义:基于相关性转换 \(d = 1 - |\rho|\)

三种链接方法

方法 原理 特点
单链接 类间最小距离 易产生链式效应
完全链接 类间最大距离 类内直径小
平均链接 类间平均距离 最常用,本例采用

金融应用:发现同行业股票、识别风格因子、地域因子分析

进阶:网络图可视化

将相关性表示为网络,直观展示股票关联结构:

展开完整代码(投影默认折叠)

import networkx as nx

threshold = 0.5  # 只保留|ρ|≥0.5的强相关
G = nx.Graph()
for stock in stocks.values():
    G.add_node(stock)

for i in range(n):
    for j in range(i+1, n):
        corr = corr_matrix.iloc[i, j]
        if abs(corr) >= threshold:
            G.add_edge(
                corr_matrix.index[i],
                corr_matrix.columns[j],
                weight=corr
            )

网络图的绘制与解读

展开完整代码(投影默认折叠)

plt.figure(figsize=(10, 8))
pos = nx.spring_layout(G, k=0.5, seed=42)
degrees = dict(G.degree())

# 节点:大小与连接数成正比
nx.draw_networkx_nodes(
    G, pos,
    node_size=[v * 300 for v in degrees.values()],
    node_color='steelblue', alpha=0.7
)
# 边:绿色=正相关,红色=负相关
weights = [G[u][v]['weight'] for u, v in G.edges()]
edge_colors = ['red' if w < 0 else 'green' for w in weights]
nx.draw_networkx_edges(
    G, pos,
    width=[abs(w) * 3 for w in weights],
    edge_color=edge_colors, alpha=0.6
)
nx.draw_networkx_labels(G, pos, font_size=10)
plt.title(f'股票相关性网络图(阈值={threshold})')
plt.axis('off')
plt.show()

网络图解读要点

网络图解读要点 四个卡片分别解释网络图中节点大小、边颜色、边粗细、节点位置的含义 节点大小 度数(连接数) 越大 = 系统重要性越高 边的颜色 绿色 = 正相关 红色 = 负相关 边的粗细 |ρ| 越大 线条越粗 节点位置 相近 = 相关性高 孤立 = 独立走势

滚动窗口相关性分析

相关系数不是恒定的,会随市场状态变化:

展开完整代码(投影默认折叠)

stock_a = '中信证券'
stock_b = '中国平安'
window = 20  # 20日滚动窗口(约1个月)

# 计算滚动相关性
rolling_corr = log_returns[stock_a].rolling(
    window
).corr(log_returns[stock_b])

fig, axes = plt.subplots(2, 1, figsize=(14, 10))

# 子图1:滚动相关系数
axes[0].plot(rolling_corr.index, rolling_corr, linewidth=2)
axes[0].axhline(y=0, color='k', linestyle='--')
axes[0].axhline(y=0.5, color='r', linestyle='--', label='阈值0.5')
axes[0].axhline(y=-0.5, color='r', linestyle='--', label='阈值-0.5')
axes[0].set_title(f'{stock_a}{stock_b}的滚动相关性')
axes[0].legend()

# 子图2:归一化价格走势
norm_prices = df_prices[[stock_a, stock_b]] / \
    df_prices[[stock_a, stock_b]].iloc[0] * 100
axes[1].plot(norm_prices[stock_a], label=stock_a)
axes[1].plot(norm_prices[stock_b], label=stock_b)
axes[1].set_title('归一化价格走势(初始=100)')
axes[1].legend()
plt.tight_layout()
plt.show()

相关性的时变特征与策略启示

市场状态对相关性的影响

  • 牛市:股票普遍同涨,相关性普遍较高
  • 熊市/危机:恐慌抛售,所有资产相关性趋近 1(“相关性崩塌”)
  • 震荡市:个股分化,相关性降低

相关性时变的策略启示

  • 不能完全依赖历史相关性来构建组合
  • 需要动态监控相关性变化
  • 极端事件时分散化可能失效
  • 2008年金融危机:几乎所有资产同向下跌

本章知识总结

本章知识总结 总结本章四大核心模块:理论基础、数据处理、可视化方法、实战应用 本章核心知识框架 理论基础 • 皮尔逊相关系数 • 投资组合方差 • 显著性检验 • 相关 ≠ 因果 数据处理 • tushare 获取数据 • 简单/对数收益率 • df.corr() 计算 • 相关系数矩阵 可视化方法 • sns.heatmap() • sns.clustermap() • 网络图 networkx • 颜色映射方案 实战应用 • 投资组合分散化 • 配对交易策略 • 滚动相关性监控 • 危机期间相关性

随堂练习

  • 问题 1|怎样完成本章分析?:独立重新计算教师参考解答中的 correlation_matrixrolling_pair:写出价格表字段、同一时间段的收益率样本量、3×3 方阵和滚动尾值;只有古典条件成立时才用 pearsonr 计算原始 p 值,并对三组资产对写出 FDR 调整后的 q 值。
  • 问题 2|结果说明什么?:针对 returns → correlation_matrix → rolling_pair,画出从数据输入、处理到结果;引用一个实际证券代码、相关系数或 p 值说明其作用,并写出交易日未对齐或窗口不足时的需要重新检查的情况。
  • 问题 3|换一个情境,怎样继续应用?:把 selected_codes 替换为另一组三只长三角公司,保持同一时间段的收益、矩阵对称性和60日滚动计算不变;写出替换字段、一条对角线为1的检查,并比较拓展应用前后方阵结构与滚动尾值。
  • 作答提示:请完成三道题,并在回答中引用实际运行结果;拓展练习中不要把课堂示例写成普遍规律或因果结论。

教师参考解答

教师参考解答|答案与说明 1

  • 所用数据与字段:规定资产 stock/stock_price_post_adjusted.parquet;长三角样本 600104.SH/600009.SH/600018.SH;字段 ts_code/trade_date/close
  • 解答示例|相关矩阵、滚动相关与显著性(平台保护块之外):

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具
import pandas as pd  # 导入表格库
from scipy.stats import pearsonr  # 导入古典皮尔逊相关检验
from statsmodels.stats.multitest import multipletests  # 导入多重检验FDR调整工具
path=Path('/home/ubuntu/r2_data_mount/data/stock/stock_price_post_adjusted.parquet')  # 指定可用资产
if not path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 缺失即停止
selected_codes=['600104.SH','600009.SH','600018.SH']  # 事先设定上海代表性非金融公司
prices=pd.read_parquet(path,columns=['order_book_id','date','close']).rename(columns={'order_book_id':'ts_code','date':'trade_date'})  # 按真实Parquet 字段结构读取并标准化字段
prices['ts_code']=prices['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 将本地证券代码统一为课堂后缀
prices=prices.loc[prices['ts_code'].isin(selected_codes)].copy()  # 筛选事先设定对象
prices['trade_date']=pd.to_datetime(prices['trade_date'])  # 统一交易日期
price_panel=prices.pivot(index='trade_date',columns='ts_code',values='close').sort_index()  # 对齐共同交易日
returns=price_panel.pct_change(fill_method=None).dropna(how='any')  # 计算同一时间段的收益率
correlation_matrix=returns.corr()  # 计算静态相关方阵
rolling_pair=returns[selected_codes[0]].rolling(60).corr(returns[selected_codes[1]])  # 计算60日滚动相关
pair_correlation,p_value=pearsonr(returns[selected_codes[0]],returns[selected_codes[1]])  # 计算同一对资产的系数与p值
pairs=[(selected_codes[0],selected_codes[1]),(selected_codes[0],selected_codes[2]),(selected_codes[1],selected_codes[2])]  # 列出三资产矩阵的全部检验族
pair_p_values=[pearsonr(returns[left_code],returns[right_code]).pvalue for left_code,right_code in pairs]  # 在古典假设成立时计算各资产对原始p值
pair_q_values=multipletests(pair_p_values,method='fdr_bh')[1]  # 对同一检验族执行Benjamini-Hochberg调整
assert correlation_matrix.shape==(3,3) and correlation_matrix.index.equals(correlation_matrix.columns)  # 核对方阵维度与标签顺序
print(returns.shape,correlation_matrix.round(3),rolling_pair.dropna().tail(),{'pair_correlation':pair_correlation,'raw_p_value':p_value,'FDR_q_values':pair_q_values})  # 输出相关、滚动与多重检验依据

教师参考解答|答案与说明 2

  • 解释答案:收益率相关用于描述同一时间段线性同动,热力图只编码矩阵结构;古典 Pearson 推断要求独立二元正态配对,金融序列不满足时应改用保留时间结构的区组 bootstrap;矩阵星号使用 FDR 调整后的 q 值。显著不等于经济重要,更不等于因果。
  • 拓展应用答案:把对象替换为另一组长三角公司,比较60/120日窗口;预期相关强度随样本和窗口变化;用矩阵对称性、对角线为1和 pearsonr 交叉核对。
  • 参考结果:同一时间段的收益率样本量、3×3 相关矩阵、60 日滚动相关的最新值、指定资产对系数、原始 p 值与三对资产的 FDR q 值。若常规检验条件不成立,可改用区组 bootstrap 置信区间。股票行情可从课程数据下载入口取得。
  • 边界 / 局限:价格相关不等于收益率相关;古典 p 值依赖采样假设;相关不等于因果
  • 常见错误:交易日未对齐;对价格水平求相关;对矩阵内原始 p 值直接附星号